/*
 * Copyright 2009-2010 the original author or authors.
 *
 * Licensed under the Apache license, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.internna.iwebjtracker.services;

import java.util.Date;
import javax.annotation.security.RolesAllowed;
import org.springframework.context.MessageSource;
import org.springframework.transaction.annotation.Transactional;
import org.directwebremoting.annotations.RemoteMethod;
import org.internna.iwebjtracker.i18n.MessageResolver;
import org.internna.iwebmvc.model.UUID;
import org.internna.iwebmvc.utils.Assert;
import org.internna.iwebmvc.model.DomainEntity;
import org.internna.iwebjtracker.project.model.Project;
import org.internna.iwebjtracker.server.Server;
import org.internna.iwebjtracker.server.ServerManager;
import org.internna.iwebjtracker.project.model.Module;
import org.internna.iwebjtracker.project.model.Version;
import org.internna.iwebjtracker.server.connection.ConnectionDetails;
import org.internna.iwebjtracker.server.scm.SourceControlManagementServer;
import org.internna.iwebjtracker.project.model.IWebJTrackerUser;

import org.internna.iwebjtracker.project.validation.Errors;
import static org.internna.iwebmvc.utils.StringUtils.hasText;
import static org.internna.iwebjtracker.project.validation.Errors.*;
import static org.internna.iwebjtracker.server.scm.NullSCMServer.NULL_SERVER_TYPE;

/**
 * Default implementation of some methods of the ProjectManager interface.
 *
 * @author Jose Noheda
 * @since 1.0
 */
public abstract class AbstractProjectManager<C extends ConnectionDetails, M extends Module<P, M> & DomainEntity, V extends Version<U, P, V> & DomainEntity, U extends IWebJTrackerUser, P extends Project<C, M, V, U, P>> extends MessageResolver implements ProjectManager<C, M, V, U, P> {

    public static final String ROLE_PROJECT_MANAGER = "ROLE_PROJECT_MANAGER";

    private final ServerManager serverManager;

    /**
     * Sets the configured servermanager instance and message resolution beans.
     *
     * @param serverManager a not null instance
     * @param messageSource any
     */
    public AbstractProjectManager(ServerManager serverManager, MessageSource messageSource) {
        super(messageSource);
        Assert.notNull(serverManager);
        this.serverManager = serverManager;
    }

    /**
     * @return the configured serverManager instance
     */
    protected final ServerManager getServerManager() {
        return serverManager;
    }

    /**
     * Adds the version (if able) and proceeds with validateProject(project).
     *
     * @param project any
     * @param version any
     * @return same as validateProject(project)
     */
    @RemoteMethod
    @Transactional
    @RolesAllowed(ROLE_PROJECT_MANAGER)
    @Override public final String validateProject(P project, String version) {
        if ((project != null) && (hasText(version))) {
            project.createVersion(version, null);
        }
        return validateProject(project);
    }

    /**
     * Checks for nullability and a valid server and delegates the work.
     *
     * @param project any project
     * @return the resolved i18n key of the known validation error or null/empty
     */
    @Override public final String validateProject(P project) {
        if (project != null) {
            loadConnectionDetails(project);
            String serverType = project.getServerType();
            if (hasText(serverType)) {
                if (NULL_SERVER_TYPE.equals(serverType) || ((project.getConnectionDetails() != null) && hasText(project.getRootPath()))) {
                    Server server = serverManager.getConfiguredServerByType(project.getServerType());
                    if (server instanceof SourceControlManagementServer) {
                        return resolve(((SourceControlManagementServer) server).validateProject(project));
                    }
                    return resolve(INVALID_SERVER_TYPE);
                }
                return resolve(NULL_CONNECTION_DETAILS);
            }
            return resolve(INVALID_SERVER_TYPE);
        }
        return resolve(NULL_PROJECT_VALIDATION);
    }

    /**
     * Entity parser (see IWebMvc) for the ConnectionDetails entity.
     *
     * @param project any
     */
    protected abstract void loadConnectionDetails(P project);

    /**
     * Persists changes to a
     *
     * @param id a valid, not null, primary key of a project entity
     * @param name a not empty string
     * @param alias a not empty string
     * @param description a not empty string
     * @param logo any
     * @param isPublic any
     */
    @RemoteMethod
    @Transactional
    @RolesAllowed(ROLE_PROJECT_MANAGER)
    @Override public final void editProjectSettings(UUID id, String name, String alias, String description, String url, UUID logo, boolean isPublic) {
        P project = loadProject(id);
        if (project != null) {
            if (hasText(name)) {
                project.setProjectName(name);
            }
            if (hasText(alias)) {
                project.setAlias(alias);
            }
            if (hasText(description)) {
                project.setDescription(description);
            }
            if (hasText(url)) {
                project.setHomePage(url);
            }
            project.setAsPublic(isPublic);
            setProjectLogo(project, logo);
            updateProject(project);
        }
    }

    /**
     * Loads a project from persistent storage.
     *
     * @param id a valid, not null, primary key of a project entity
     * @return a project entity
     */
    protected abstract P loadProject(final UUID id);

    /**
     * Saves changes in the entity in the database.
     *
     * @param project any
     */
    protected abstract void updateProject(P project);

    /**
     * Changes the logo of the project based on this new entity (if any).
     *
     * @param logo any
     */
    protected abstract void setProjectLogo(P project, UUID logo);

    /**
     * Creates a module or component (if a parent module is provided) and persists it.
     *
     * @param project any
     * @param module any (higher priority than project)
     * @param name any
     * @param description any
     * @return any
     */
    @RemoteMethod
    @Transactional
    @RolesAllowed(ROLE_PROJECT_MANAGER)
    @Override public final UUID createModule(UUID project, UUID module, String name, String description) {
        UUID generatedId = null;
        if (hasText(name) && hasText(description)) {
            if (module != null) {
                generatedId = createComponent(module, name, description);
            } else if (project != null) {
                generatedId = createModule(project, name, description);
            }
        }
        return generatedId;
    }

    private UUID createModule(UUID project, String name, String description) {
        UUID generatedId = null;
        P prj = loadProject(project);
        if (prj != null) {
            M module = prj.createModule(name, description);
            persistModule(module);
            generatedId = module.getId();
        }
        return generatedId;
    }

    private UUID createComponent(UUID module, String name, String description) {
        UUID generatedId = null;
        M mod = loadModule(module);
        if (mod != null) {
            M component = mod.createComponent(name, description);
            persistModule(component);
            generatedId = component.getId();
        }
        return generatedId;
    }

    /**
     * Changes the name and description as long as they are not empty and a valid module was loaded.
     *
     * @param id any
     * @param name any
     * @param description any
     */
    @RemoteMethod
    @Transactional
    @RolesAllowed(ROLE_PROJECT_MANAGER)
    @Override public final void changeModule(UUID id, String name, String description) {
        M module = loadModule(id);
        if (module != null) {
            if (hasText(name)) {
                module.setModuleName(name);
            }
            if (hasText(description)) {
                module.setDescription(description);
            }
            persistModule(module);
        }
    }

    /**
     * Obtains the module from persistent storage.
     *
     * @param id any
     * @return any
     */
    protected abstract M loadModule(UUID id);

    /**
     * Persists any change in the module entity (include creation of the module).
     *
     * @param module any
     */
    protected abstract void persistModule(M m);

    /**
     * Loads the objects from persistent storage and delegates to the project to perform the actual logic.
     *
     * @param project any
     * @param trackerUser any
     * @param asAdministrator true if the user can update the project configuration
     * @return true if the user has been really added to the project members
     */
    @RemoteMethod
    @Transactional
    @RolesAllowed(ROLE_PROJECT_MANAGER)
    @Override public final boolean addProjectMember(UUID project, UUID trackerUser, boolean asAdministrator) {
        boolean added = false;
        P prj = loadProject(project);
        U user = loadTrackerUser(trackerUser);
        if ((prj != null) && (user != null)) {
            added = prj.addMember(user, asAdministrator);
            if (added) {
                onAddProjectMember(prj, user, asAdministrator);
                updateProject(prj);
            }
        }
        return added;
    }

    /**
     * Extension point when a user is added to the project with a role.
     *
     * @param project a non null instance
     * @param user a non null instance
     * @param asAdministrator true if the user was added as administrator (and developer)
     */
    protected abstract void onAddProjectMember(P project, U user, boolean asAdministrator);

    /**
     * Obtains the application user from persistent storage.
     *
     * @param id any
     * @return any
     */
    protected abstract U loadTrackerUser(UUID id);

    /**
     * Checks for a parent version or parent project (version has priority) and
     * delegates to it.
     *
     * @param project any
     * @param name any
     * @param due any
     * @return any
     */
    @RemoteMethod
    @Transactional
    @RolesAllowed(ROLE_PROJECT_MANAGER)
    @Override public final UUID createVersion(UUID project, UUID version, String name, Date due) {
        UUID generatedId = null;
        if (version != null) {
            generatedId = createMilestone(version, name, due);
        } else {
            generatedId = createVersion(project, name, due);
        }
        return generatedId;
    }

    private UUID createVersion(UUID project, String name, Date due) {
        UUID generatedId = null;
        P prj = loadProject(project);
        if ((prj != null) && (hasText(name))) {
            V version = prj.createVersion(name, due);
            persistVersion(version);
            generatedId = version.getId();
        }
        return generatedId;
    }

    private UUID createMilestone(UUID version, String name, Date due) {
        UUID generatedId = null;
        V parentVersion = loadVersion(version);
        if ((parentVersion != null) && (hasText(name))) {
            V milestone = parentVersion.createMilestone(name, due);
            persistVersion(milestone);
            generatedId = milestone.getId();
        }
        return generatedId;
    }

    /**
     * Obtains a valid version object from persistent storage.
     *
     * @param version any
     * @return any
     */
    protected abstract V loadVersion(UUID version);

    /**
     * Persists any change in the Version entity (include creation of the version).
     *
     * @param version any
     */
    protected abstract void persistVersion(V version);

    /**
     * Checks for valid id and label and applies the new values.
     *
     * @param id any (no action performed if null)
     * @param label any (no action performed if null or empty)
     * @param due any
     */
    @RemoteMethod
    @Transactional
    @RolesAllowed(ROLE_PROJECT_MANAGER)
    @Override public final void changeVersion(UUID id, String label, Date due) {
        V version = loadVersion(id);
        if ((version != null) && hasText(label)) {
            version.setLabel(label);
            version.setDueDate(due);
            persistVersion(version);
        }
    }

    /**
     * Checks for valid project and version and delegates the logic to the project.
     *
     * @param project any (no action performed if null)
     * @param version any (no action performed if null)
     */
    @RemoteMethod
    @Transactional
    @RolesAllowed(ROLE_PROJECT_MANAGER)
    @Override public final boolean setAsCurrentVersion(UUID project, UUID version) {
        boolean updated = false;
        P prj = loadProject(project);
        if (prj != null) {
            V currentVersion = loadVersion(version);
            if ((currentVersion != null) && prj.setAsCurrentVersion(currentVersion)) {
                updateProject(prj);
                updated = currentVersion.isCurrentVersion();
            }
        }
        return updated;
    }

    /**
     * Validates project and delegates closing logic to the project itself.
     *
     * @param project any
     * @return the i18n translated error or null
     */
    @RemoteMethod
    @Transactional
    @RolesAllowed(ROLE_PROJECT_MANAGER)
    @Override public final String closeProject(UUID project) {
        return openOrClose(project, true);
    }

    /**
     * Validates project and delegates opening logic to the project itself.
     *
     * @param project any
     * @return the i18n translated error or null
     */
    @RemoteMethod
    @Transactional
    @RolesAllowed(ROLE_PROJECT_MANAGER)
    @Override public final String reopenProject(UUID project) {
        return openOrClose(project, false);
    }

    private String openOrClose(UUID project, boolean close) {
        P prj = loadProject(project);
        if (prj != null) {
            if (prj.isClosed() != close) {
                return resolve(proceedWithOpenOrClose(prj, close));
            }
            return resolve(close ? PROJECT_CLOSED : PROJECT_OPEN);
        }
        return resolve(PROJECT_DOES_NOT_EXIST);
    }

    /**
     * Closes or reopens a valid project (till the moment)
     *
     * @param project a non null instance
     * @param close true if the project should be closed, false to reopen it
     * @return any
     */
    protected abstract Errors proceedWithOpenOrClose(P project, boolean close);

    /**
     * Validates version and project delegating closing logic to the actual version.
     *
     * @param version any
     * @return the i18n translated error or null
     */
    @Override public final String closeVersion(UUID version) {
        V versionToClose = loadVersion(version);
        if (versionToClose != null) {
            P project = versionToClose.getParentProject();
            if (!project.isClosed()) {
                if (!versionToClose.isClosed()) {
                    return resolve(proceedWithVersionClose(versionToClose));
                }
                return resolve(VERSION_CLOSED);
            }
            return resolve(PROJECT_CLOSED);
        }
        return resolve(NULL_VERSION);
    }

    /**
     * Closes a valid version (till the moment).
     *
     * @param version a non null instance
     * @return any
     */
    protected abstract Errors proceedWithVersionClose(V version);

}
